
/*
    LineGraph.m

    LineGraph is a simple view which plots a set of points as a connected line.
    LineGraph draws the graph using a userpath.  It retains the two arrays
    that make up the userpath: the coordinates of the userpath and the
    userpath operators (e.g., dps_lineto).

    You may freely copy, distribute, and reuse the code in this example.
    NeXT disclaims any warranty of any kind, expressed or implied, as to its
    fitness for any particular use.
*/

#import "Graph.h"

static void drawAxes(float x1, float y1, float x2, float y2);
static void testBounds(float *min, float *max, float *points, int num);

#define POS_INFINITY	(1.0/0.0)
#define NEG_INFINITY	(-1.0/0.0)
#define IS_NAN(x)	((x)!=(x))	/* IEEE test for NAN (not a number) */
#define IS_STRANGE(x)	(IS_NAN(x) || x == POS_INFINITY || x == NEG_INFINITY)


@implementation LineGraph

- initFrame:(NXRect *)aRect {
    [super initFrame:aRect];
    lineGray = NX_WHITE;
    backgroundGray = NX_BLACK;
    return self;
}

- free {
    NXZoneFree([self zone], points);
    NXZoneFree([self zone], ops);
    return [super free];
}

- setPoints:(int)num x:(float *)x y:(float *)y
	minX:(float)minX minY:(float)minY maxX:(float)maxX maxY:(float)maxY {
    float *f;
    int i;
    char *op;
    NXZone *zone;
    int segPoints;

 /*
  * We copy these points into our own an array that we later use when
  * we draw them as a userpath.  This array has the x and y values
  * interleaved.  We also generate an array of userpath operators that we
  * will use to draw the userpath.  These operators are just a moveto to
  * get to the start of the line, and then lineto's connecting all the points
  * from there on.
  *
  * We also filter out the troublesome floating point values of positive
  * infinity, negative infinity and NAN ("not a number").  When we encounter
  * these values, we skip that particular point and remember to start a new
  * segment of the graph when we see the next well behaved value.
  */
    zone = [self zone];
    NXZoneFree(zone, points);
    NXZoneFree(zone, ops);
    numPoints = num;
    points = NXZoneMalloc(zone, 2 * num * sizeof(float));
    ops = NXZoneMalloc(zone, num * sizeof(char));
    numPoints = 0;
    segPoints = 0;
    for (f = points, op = ops, i = num; i--; ) {
	if (IS_STRANGE(*x) || IS_STRANGE(*y)) {
	    if (segPoints == 1) {
	      /*
	       * This is a the case of a well behaved point with weird values
	       * on either side.  In this case we just make a tiny line
	       * segment of this point to itself to.
	       */
		*f++ = f[-2];
		*f++ = f[-2];
		*op++ = dps_lineto;
		numPoints++;
	    }
	    segPoints = 0;
	    x++;
	    y++;
	} else {
	    *f++ = *x++;
	    *f++ = *y++;
	    if (segPoints == 0)
		*op++ = dps_moveto;
	    else
		*op++ = dps_lineto;
	    segPoints++;
	    numPoints++;
	}
    }
    if (numPoints > 0) {
	testBounds(&minX, &maxX, points, numPoints);
	bbox[0] = minX;
	bbox[2] = maxX;
	testBounds(&minY, &maxY, points+1, numPoints);
	bbox[1] = minY;
	bbox[3] = maxY;
    } else {
	bbox[0] = bbox[1] = -1;
	bbox[2] = bbox[3] = 1;
    }
    return self;
}

- sizeTo:(NXCoord)width :(NXCoord)height {
    NXPoint center;

 /*
  * We override sizeto so that after we're resized the point at the center
  * of the view remains the same.  We just note the center, pass the message
  * up to the super class, and then reset the center to what it was.
  */
    center.x = NX_X(&bounds) + NX_WIDTH(&bounds) / 2;
    center.y = NX_Y(&bounds) + NX_HEIGHT(&bounds) / 2;
    [super sizeTo:width :height];
    [self setDrawOrigin:center.x - NX_WIDTH(&bounds) / 2
			:center.y - NX_HEIGHT(&bounds) / 2];
    return self;
}

- scaleToFit {
    float scale;
    float bbWidth = bbox[2] - bbox[0];
    float bbHeight = bbox[3] - bbox[1];

 /*
  * First we figure out how much we need to scale so that our current graph
  * will just fit within the view.  We do this by taking the ratio of the
  * the view's current size to the bounding box of the graph.  We also add
  * a little fudge factor of 0.95 so the graph will have a little border
  * space around it.
  */
    if (bbWidth == 0 && bbHeight == 0)
	scale = 10;
    else if (bbWidth == 0)
	scale = NX_HEIGHT(&frame) / bbHeight * 0.95;
    else if (bbHeight == 0)
	scale = NX_WIDTH(&frame) / bbWidth * 0.95;
    else
	scale = MIN(NX_WIDTH(&frame) / bbWidth,
		    NX_HEIGHT(&frame) / bbHeight) * 0.95;

  /* scale the size of the view */
    [self setDrawSize:NX_WIDTH(&frame) / scale :NX_HEIGHT(&frame) / scale];

  /*
   * translate the view so the graph's bounding box is in the lower left corner
   */
    [self setDrawOrigin:bbox[0] - (NX_WIDTH(&bounds) - bbWidth) / 2
			:bbox[1] - (NX_HEIGHT(&bounds) - bbHeight) / 2];
    return self;
}

- zoom:(float)scale {
    NXPoint center;

  /* remember the center to we can reset it after the scaling */
    center.x = NX_X(&bounds) + NX_WIDTH(&bounds) / 2;
    center.y = NX_Y(&bounds) + NX_HEIGHT(&bounds) / 2;

  /* scale the view to its new size */
    [self setDrawSize:NX_WIDTH(&bounds) / scale :NX_HEIGHT(&bounds) / scale];

  /* reset the center point */
    [self setDrawOrigin:center.x - NX_WIDTH(&bounds) / 2
			:center.y - NX_HEIGHT(&bounds) / 2];
    return self;
}

/*
 * In both the drawSelf method and the drawAxes function, we use a little
 * trick to get the right line width regardless of how much we are zoomed
 * in.  If we are drawing on the screen, we just draw the lines with zero
 * width.  This the fastest way to draw lines, and ensures they will always
 * one pixel wide.  When drawing on the printer, we do the trick of
 * setting the matrix to the default matrix after the path has been made
 * but before it is stroked.  Since the line width is actually used when
 * the path is stroked, this makes the line width be the same regardless of
 * what scale we were at when we stroked the path.  The purpose of this is
 * to make the line width independent of how far in or our we are zoomed.
 */

- drawSelf:(const NXRect *)rects :(int)rectCount {
    PSsetgray(backgroundGray);
    NXRectFill(&bounds);
    drawAxes(NX_X(&bounds), NX_Y(&bounds), NX_MAXX(&bounds), NX_MAXY(&bounds));
    if (points && numPoints > 0) {
	PSnewpath();
	PSsetlinewidth(NXDrawingStatus == NX_DRAWING ? 0.0 : 1.0);
	PSsetgray(lineGray);
	if (NXDrawingStatus == NX_DRAWING)
	  /* stroke the userpath */
	    DPSDoUserPath(points, numPoints * 2, dps_float, ops, numPoints,
							bbox, dps_ustroke);
	else {
	  /* append the userpath to the current path, but dont stroke yet */
	    DPSDoUserPath(points, numPoints * 2, dps_float, ops, numPoints,
							bbox, dps_uappend);
	  /* trick to ensure line width is independent of zoom */
	    PSgsave();
	    PSmatrix();
	    PSdefaultmatrix();
	    PSsetmatrix();
	    PSstroke();
	    PSgrestore();
	}
    }
    return self;
}


/*
 * Simple line drawer, used to draw the axes of the graph in the current
 * color and line width.
 */
static void drawAxes(float x1, float y1, float x2, float y2) {
    PSsetlinewidth(NXDrawingStatus == NX_DRAWING ? 0.0 : 0.4);
    PSsetgray(NX_DKGRAY);
    PSnewpath();
    PSmoveto(0.0, y1);
    PSlineto(0.0, y2);
    PSmoveto(x1, 0.0);
    PSlineto(x2, 0.0);
    if (NXDrawingStatus != NX_DRAWING) {
      /* trick to ensure line width is independent of zoom */
	PSgsave();
	PSmatrix();
	PSdefaultmatrix();
	PSsetmatrix();
	PSstroke();
	PSgrestore();
    } else
	PSstroke();
}

- setLineGray:(float)gray {
    lineGray = gray;
    return self;
}

- setBackgroundGray:(float)gray {
    backgroundGray = gray;
    return self;
}

- (float)lineGray {
    return lineGray;
}

- (float)backgroundGray {
    return backgroundGray;
}

/*
 * Tests the bounds for NAN or infinite values.  If there are such values,
 * we recalc the bounds with the given points.  Since these points are
 * the interleaved x-y pairs for the userpath, we skip every other value,
 * since we want to only consider x's or y's.
 */
static void testBounds(float *min, float *max, float *points, int num) {
    if (IS_STRANGE(*min) || IS_STRANGE(*max)) {
	*min = *max = *points;
	while (--num) {
	    points += 2;
	    if (*points > *max)
		*max = *points;
	    else if (*points < *min)
		*min = *points;
	}
    }
}

@end
